/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once

#include <baja/common.hpp>
#include <baja/graphics.hpp>
#include <baja/app/app.hpp>
#include <baja/app/var_console.hpp>
#include <baja/app/debug_console.hpp>
#include <baja/app/reference_render_object.hpp>
#include <baja/app/camera.hpp>

namespace baja { namespace app {

namespace g = baja::graphics;

class graphics_app : public iapp
{
public:
	BAJA_DECLARE_TAG("002729D4-16AB-40C5-BDCB-23D17650E33A")

protected:
	graphics_app(
		const std::shared_ptr<iapp_shell>& shell,
		const std::shared_ptr<igraphics_factory>& graphicsFactory
		) :
		_shell(shell), 
		_graphicsFactory(graphicsFactory),
		_closed(false)
		{}

	std::shared_ptr<g::iengine> getGraphics() { return _graphics; }
	std::shared_ptr<icamera> camera() { return std::static_pointer_cast<icamera>(_camera); }

	virtual void onUpdate() = 0;

public:
	virtual ~graphics_app() {}

	void onInitialize()
	{
		this->safeExecute([&]()
		{
			// create graphics
			_graphics = _graphicsFactory->createGraphics(_shell.lock());
			services::set<graphics_app>(_graphics);

			// create render service
			_renderService = g::createRenderService();
			services::set<graphics_app>(_renderService);

			// create var console and root var group
			_varRoot = createVarGroup(L"root");
			_varConsole = createVarConsole(_varRoot);
			_varRoot->addChildGroup(_graphics->getVarGroup());
			services::set<graphics_app>(_varConsole);

			// add var console's render object
			_renderService->addRenderObject(std::dynamic_pointer_cast<g::irender_object>(_varConsole));

			// create debug console and debug var group
			_debugRoot = createVarGroup(L"debug");
			_debugConsole = createDebugConsole(_debugRoot);
			services::set<graphics_app>(_debugConsole);

			// add debug console's render object
			_renderService->addRenderObject(std::dynamic_pointer_cast<g::irender_object>(_debugConsole));

			// create the camera
			_camera = createOriginCamera(_graphics->getCoordinateSystemMatrix());

			// create references render object
			_referenceRenderObject = reference_render_object::create(_camera);
			_renderService->addRenderObject(std::dynamic_pointer_cast<g::irender_object>(_referenceRenderObject));

			// initialize render service
			_renderService->initialize(_graphics);

			// add debug vars
			_frameCount = 0;
			_debugRoot->addIntVar(
				L"frame count",
				&_frameCount,
				0, 0, 0, 0
				);
		});
	}

	void onRun()
	{
		this->safeExecute([&]()
		{
			std::shared_ptr<iapp_shell> shell = _shell.lock();
			while (!_closed)
			{
				_graphics->clear(g::clear_targets::colorDepthAndStencil, g::packColor(0x0, 0x0, 0x0), 1.f, 0);
				_graphics->beginScene();

				_renderService->render(_graphics);

				_graphics->endScene();
				_graphics->showScene();

				_frameCount++;
				this->onUpdate();
			}
		});
	}

	void onUninitialize()
	{
		this->cleanup();
	}

	void terminate()
	{
		_closed = true;
	}

	void onResize(const resize_args& args)
	{
		this->safeExecute([&]()
		{
			g::display_params dp = _graphics->getDisplayParams();
			dp.backBufferWidth = args.width;
			dp.backBufferHeight = args.height;

			_graphics->changeDisplayParams(dp);
			_renderService->resize(_graphics);
		});
	}

	// iinput_observer

	void onPointer(const pointer_args& args)
	{
		wchar* type = 
			(args.type == pointer_event_type::down) ? L"down" :
			(args.type == pointer_event_type::move) ? L"move" :
			L"up";

		wchar* deviceType = 
			(args.deviceType == device_type::mouse) ? L"mouse" :
			(args.deviceType == device_type::touch) ? L"touch" :
			L"pen";

		boost::wformat fmt = 
			boost::wformat(L"pointer %s, %s: pre: %0.2f, tim: %i, pos: (%0.2f, %0.2f)") %
			type %
			deviceType %
			args.pressure %
			args.timestamp %
			args.point.x %
			args.point.y;

		_debugConsole->trace(fmt.str().c_str());
	}

	void onGesture(const gesture_args& args)
	{
		bool rotating = false;
		bool expanding = false;

		wchar* type = 
			(args.type == gesture_event_type::start) ? L"start" :
			(args.type == gesture_event_type::update) ? L"update" :
			L"complete";

		wchar* deviceType = 
			(args.deviceType == device_type::mouse) ? L"mouse" :
			(args.deviceType == device_type::touch) ? L"touch" :
			L"pen";

		boost::wformat fmt = 
			boost::wformat(L"gesture %s, %s: exp: %0.2f, rot: %0.2f, tra: (%0.2f, %0.2f)") %
			type %
			deviceType %
			args.delta.expansion %
			args.delta.rotation %
			args.delta.translation.x %
			args.delta.translation.y;

		_debugConsole->trace(fmt.str().c_str());

		if (args.type != gesture_event_type::update)
		{
			return;
		}

		if (args.delta.expansion != 0)
		{
			expanding = true;
			_camera->setDistance(_camera->distance() + -args.delta.expansion * 0.35);
		}

		if ((args.delta.rotation != 0))
		{
			rotating = true;
			_camera->rotateDeltaXYZ(0, 0, 0.05 * m::radToDeg(args.delta.rotation));
		}

		if (!expanding && !rotating && 
			(args.delta.translation.x >= 0.5 || 
			 args.delta.translation.y >= 0.5 || 
			 args.delta.translation.x <= -0.5 || 
			 args.delta.translation.y <= -0.5))
		{
			_camera->rotateDeltaXYZ(
				-0.40f * args.delta.translation.y, 
				0, 
				-0.40f * args.delta.translation.x
				);
		}
	}

	void onKey(const key_args& args)
	{
		switch (args.vk)
		{
		case (192): // ~
			_varConsole->setIsVisible(!_varConsole->getIsVisible());
			break;

		case (9): // tab
			_debugConsole->setIsVisible(!_debugConsole->getIsVisible());
		}

		if (_varConsole->getIsVisible())
		{
			switch (args.vk)
			{
			case (38): // up
				_varConsole->moveSelectionUp();
				break;

			case (40): // down
				_varConsole->moveSelectionDown();
				break;

			case (37): // left
				_varConsole->decrementSelection();
				break;

			case (39): // right
				_varConsole->incrementSelection();
				break;

			case (13): // enter
				_varConsole->activateSelection();
				break;

			case (8): // back
				_varConsole->navigateBack();
				break;
		
			case (27): // escape
				_varConsole->navigateBack();
				break;
			}
		}
	}

private:

	void cleanup()
	{
		if (_renderService.get() != nullptr)
		{
			// tear down render service
			_renderService->teardown(_graphics);

			// TEST: make sure this works
			// remove var console from render service
			if (_varConsole)
				_renderService->removeRenderObject(std::dynamic_pointer_cast<g::irender_object>(_varConsole));

			if (_debugConsole)
				_renderService->removeRenderObject(std::dynamic_pointer_cast<g::irender_object>(_debugConsole));

			if (_referenceRenderObject)
				_renderService->removeRenderObject(std::dynamic_pointer_cast<g::irender_object>(_referenceRenderObject));
		}

		// release reference render object
		_referenceRenderObject.reset();

		// release camera
		_camera.reset();

		// release debug console
		services::set<graphics_app>(std::shared_ptr<idebug_console>());
		_debugConsole.reset();
		_debugRoot.reset();

		// release var console and var groups
		services::set<graphics_app>(std::shared_ptr<ivar_console>());
		_varConsole.reset();
		_varRoot.reset();

		// release render service
		services::set<graphics_app>(std::shared_ptr<g::irender_service>());
		_renderService.reset();

		// release graphics
		services::set<graphics_app>(std::shared_ptr<g::iengine>());
		_graphics.reset();
	}

	void safeExecute(std::function<void()> code)
	{
		try
		{
			code();
		}
		catch (const std::exception& e)
		{
			this->cleanup();

			BAJA_TRACE("!!ERROR!!: metro_shell: unhandled error happened.  terminating program.");
			BAJA_TRACE_EXCEPTION(e);

			// end running the app
			_closed = true;
		}
	}

private:

	std::weak_ptr<iapp_shell> _shell;
	std::shared_ptr<igraphics_factory> _graphicsFactory;
	std::shared_ptr<g::iengine> _graphics;
	std::shared_ptr<g::irender_service> _renderService;
	
	std::shared_ptr<ivar_console> _varConsole;
	std::shared_ptr<ivar_group> _varRoot;
	std::shared_ptr<idebug_console> _debugConsole;
	std::shared_ptr<ivar_group> _debugRoot;

	std::shared_ptr<reference_render_object> _referenceRenderObject;
	std::shared_ptr<iorigin_camera> _camera;
	bool _closed;

	int32 _frameCount;
};

}} // namespace baja::app